home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
Cream of the Crop 25
/
Cream of the Crop 25.iso
/
bbs
/
doorinfo.zip
/
DOORUTIL.MAN
< prev
next >
Wrap
Text File
|
1997-05-14
|
74KB
|
1,564 lines
FALKEN Programmers Guide
by Chris Whitacre
Copywrite BC Software 1997
Can be used for Falken 7.07 or 9.x
NOTE: Any other info if needed can be obtained by either emailing
cd@midnight.kingsnet.com, telnet midnight.kingsnet.com, or calling
(209) 582-4747.
What It Is.
-----------
This document contains programming information for Falken BBS. This
documentation is actually little more than a collection of notes
that have been gathered together into more or less a logical
presentation on the subject of Falken doors.
Falken is a multi-line BBS package that runs under DOS on IBM PC and
compatible computers, 80386 and above. It can support up to 64
modems simultaneously.
External modules, called DOORS in other BBS jargon, are supported.
Because Falken contains a multitasker and a Fossil-compatible
multi-line serial driver, most DOORS for single-line BBS packages
will not run correctly under Falken. That is because the programmers
for those doors often do not use the existing serial drivers, but
install their own hardware interrupt functions. Also, many of them
reprogram the computer's clock, and write directly to the video
screen, and many other things that are fine for a one-line BBS to
do, but are not fine for multi-line BBSes to do.
Remember that for Falken to work correctly, the BBS program MUST
retain control of all the hardware and all the interrupts, so that
it can service all of the modems.
A door may be started from the Main Menu of Falken, or from any of
the sub-menus. Simply place the name of the program to execute in
the 'action' field of the menu screen in BBSCFG. Falken internal
functions such as email and files are identified with a '@' in the
first column, i.e. @EMAIL or @TLCF. If the '@' is not in the first
column, then Falken assumes the entry to be the name of a door
program to execute.
If you write a door program for Falken BBS systems, and choose to
place it in the public domain, or distribute it as shareware, you
may forward a copy to me to be included with Falken updates and
purchases. All programs received by me will be treated as either
public domain or shareware, as indicated by the author. Not all
programs that are received will be distributed by me, but all will
be placed on our customer support BBS for access by Falken sysops.
So, here is a programming API and a set of guidelines for writing
games and utilities for Falken BBS.
I believe that writing games for Falken is easier than writing games
for other BBSes. Particularly for other multi-line BBSes. I have
written a set of functions that performs all of the difficult stuff.
Getting input from the user, sending text to the user, broadcasting
to multiple lines, and many other functions are provided in simple
function calls. Indeed, there is even a printf()-like function for
sending formatted output to the modem.
If you would like specific information on writing or porting
programs to Falken, contact me at the addresses listed below.
How To Use This Document.
-------------------------
First, read it completely. All the way through. Some things that
appear unclear at first are explained in later sections of the
document. As you see more and more examples of code, and more
descriptions of how things work, the details will become more clear.
Next, look at the example programs that are provided. I have
included 3 very important programs with this document. They are :
zmsend.c - full source code for a Zmodem transfer program
tlcfdemo - full source code for a multi-user chat area
playxwit.c - full source code for a program loader for
croswits, a multiplayer word game.
These programs demonstrate some of the really useful methods for
writing games for Falken - direct I/O via Fossil calls, multiplayer
games, and database usage.
Next, write something. I firmly believe that the way to master any
language or API is to jump in and DO IT. The calls are easy, and it
really is the best way to learn.
Legal Stuff.
------------
All the information included in this package is correct, to the best
of my knowledge, as of 08-15-95. All the information that is
presented here, and all source code presented in this package, is
made freely available by me, Herb Rose. I am the sole owner of this
document and the source code that is included. I give my permission
for anyone to use the information and code herein for any purpose
that they see fit. I offer no warranty of any kind, nor any
guarantee of correctness, completeness, or fitness for any
application. In other words - here it is, use it if you wish, but
use it at your own risk.
If any errors or omissions are found in this document, or if you
desire further information or clarification, I can be reached at
rose@dgsys.com via internet, or by sending email to 'herb' on The
Doctor's Office BBS at 703-749-2860.
If you are having trouble getting something to work, PLEASE send me
the offending source code. I will not answer letters that state
"such-and-such function does not work for me" unless you send the
source code with it. Trust me when I say that I use ALL these
functions in my programs, and they work as presented. I will keep
all such source code confidential, and will probably erase it as
soon as I determine the problem, anyway.
General Notes concerning doors.
-------------------------------
First, since your door program will not have control of the PC, and
since it must share resources with Falken and with other door
programs, some popular PC programming techniques must not be used.
First and foremost, your door must never attempt to control any of
the PC interrupt vectors or hardware. It must not install its own
serial interrupt handler. It must not write directly to the display
screen. It must not read the keyboard for input. All hardware and
interrupts must be under control of Falken, or nothing will work
correctly.
Second, your door must be as small as possible. In general, try to
keep your program from using more than 256K memory. This can be
difficult at times, but it can usually be done. Keep large arrays
on disk, rather than in memory. Do not load in large data files.
Cut corners wherever you can to conserve space. Remember that
Falken is very large, and your program must fit into whatever memory
is left over after Falken is loaded.
Third, Falkens multitasker does not support overlays, so you cannot
use overlays in your program in any form.
Fourth, the Falken multitasker provides a scheduler which allows the
different active programs to run in round-robin fashion. Whenever
your program is non-productive, i.e. waiting for the user to press a
key or for a block of input to be ready for processing, you should
use the RELINQ() or DELAY() calls to remove the program from
execution, and let another task run for a while. The time slice
interval is 18.2 times per second under Falken, which means your
program could be just sitting in a loop for 1/18 second, doing
nothing, while other programs are waiting their turn to do some
work. Be conservative with system resources, so that the overall
Falken system does not degrade because of your program.
Call init() to load up the global variables and to check that the
BBS is up and running before you do anything else.
Use the standard functions that are provided for communication
between processes and between your task and the BBS. (see below).
The door program should continually monitor its input queue to
handle special commands that Falken may send to it. If the
'terminate' command is received, the door should stop processing and
exit. The qgets() and tqgets() functions monitor for the terminate
event, but if your door does not use these functions, it must
monitor for the events explicitly.
If you require character-by-character control of the serial port, or
do not wish for Falken to perform text-related character
translations, you will need to request control of the serial port.
Use the setbinarymode() function to gain control of the serial port.
Falken will disconnect from the serial port, leaving your door free
to manage the port directly. Note that when I say 'directly', I do
not mean at the hardware level. I mean that your task now has the
responsibility to control the port using FOSSIL calls via INT 14H.
See the accompanying FOSSIL documentation for more information on
this.
Rules for writing doors.
------------------------
Don't mess with lines that you don't own.
Don't mess with memory you don't own.
Don't mess with any hardware or interrupts, EVER.
Don't change the value of 'who' unless you're sure of the outcome.
If you open any files or databases, close them when you're done.
Say 'please' and 'thank you' often.
Go to the potty before a long car trip.
Sample Code.
------------
I am including sample code, including some working doors that I have
written specifically for that purpose, with this package. Among the
most useful of these samples are the files LOADTLCF.C and
TLCFDEMO.C. These programs comprise a multi-user teleconference
program, and can be the basis for multi-player games or other
programs. These programs use the method of having a small 'loader'
program run first. This 'loader' program checks to see if the real
program is already active, and if so just suspends itself. If the
real program is not already active, the loader program first loads
it, then suspends itself. The real program then monitors all lines
to see which ones are active for it, and handles the input and
output for those lines. This is done in a large loop in the main()
function. Almost all the current multi-player games use this
approach.
I am also including the complete source code for the CROSWITS loader
program, because it contains a lot of database access code via the
btrv() functions. The PLAYXWIT.C file is for study purposes only,
and will not compile because I have not included some of the header
files.
Header Files.
-------------
There are several header (.h) files included with this package.
The more important ones are :
structs.h - contains account record structures, and the
user_rec structure used to store information about
the caller's current status.
doorutil.h - contains prototypes for the functions, and
contains definitions for the various variables and
structures used by door programmers.
bbscfg.h - contains the structure definition for the
cfg_rec structure. Usually, a programmer does not
need this information, but if you do need it, there
it is.
tcb.h - structure definitions and constants used by CSWITCH,
the multitasking engine for Falken.
Doorutil.h #includes structs.h and bbscfg.h so you only need
'#include "doorutil.h"' in your program. If you need access to the
CSWITCH structures (rarely) then include tcb.h also.
Object Files.
-------------
The following object files are supplied, in Microsoft C format. All
.obj files were compiled in LARGE model, with structure packing,
meaning that all variables are aligned on byte boundaries, not
aligned on the nearest word or dword boundary.
doorutil.obj - contains all the functions described below.
lmtca.obj - contains interface functions for CSWITCH API.
fsubs.obj - contains interface functions for the Fossil driver API.
You must link with doorutil.obj and lmtca.obj. If you will be using
the fossil calls, then link with fsubs.obj also.
I have also included the source for lmtca.obj (in MASM 5.1) and
fsubs.obj (in MASM 5.1).
Global Variables.
-----------------
The variables :
int who; /* the BBS line that started the door */
int numlines; /* number of lines configured */
int inq; /* message queue number to read messages from */
int outq; /* message queue number to send messages to BBS */
struct acct_rec *acct;
/* address of array of acct_rec structures */
struct user_rec *user;
/* address of array of user_rec structures */
struct acct_rec *myacct; /* pointer to the account record for */
/* the user that started the door. */
/* equivalent to (*acct)[who] */
struct user_rec *myuser; /* pointer to the user_rec record for */
/* the user that started the door. */
/* equivalent to (*user)[who] */
struct bbscfg_rec *cfg; /* pointer to the bbscfg record */
are initialized in the init() function, and should not be altered.
The only exception is the 'who' variable, which may be altered to
cause the output from qprintf() et al, to go to a line other than
the one that started the door.
Communications with the BBS.
----------------------------
All communications with the BBS are performed via message queues and
a shared memory structure. As long as you use the functions
outlined here, all that is done for you, by magic.
The global variable 'who' is used in several of the following
functions, notably qgets(), send(), qputs(), qprintf() to send
output to the line that started the door. Multi-player games, etc,
may want output to go to lines other than the original starter. See
functions sendtoline() and broadcast() to do this. Alternatively,
you can change the value of who prior to calling one of the other
functions that use it to determine destination.
Other functions, such as setbinarymode(), expect a line number as an
input parameter. Normally, you will use the 'who' variable, since
you will be performing these functions for the line that started the
door, so the call would be ' setbinarymode(who);'.
The following conventions will be used to pass information to and
from the MAINBBS task and subtasks :
When the subtask (your door) starts, it will read a message from
inter-task message queue #1. This message will contain all the
information the subtask requires to execute properly. It will
contain the following information, in this format :
struct init_msg
{
int type; /* will always be 7 */
int line; /* the line number for this task (0-5) */
int input_q; /* the queue to read incoming data from */
int output_q; /* the queue to send outgoing data to */
acct_rec far *acctptr;
user_rec far *userptr;
int numlines; /* how many lines are configured */
struct cfg_rec far *cfg; /* pointer to BBSCFG record */
} init;
The acctptr and userptr are pointers to an array of structures. They
are always FAR pointers, meaning they contain both segment and
offset values. If you use the small memory model to compile your
program, you must explicitly declare these pointers as FAR.
They should be accessed as follows :
To get the user's name from the account record :
init.acctptr[line].acctname /* acctname is a char array */
Refer to the STRUCTS.H file for the format of the user record and
the account record.
All messages received after this initial message will be read from
the task's input_q. All messages your task wants to send to the
user must be sent via your tasks output_q.
Use the functions in DOORUTIL.C to communicate with Falken, rather
than using the message queues directly. All functions which are
available to a door program have already been included in DOORUTIL.
NOTE: The time and date fields in account records use a special
format used to measure the current date from a fixed
reference, in this case 1 Jan 1900. A C function is supplied
to translate dates to/from a string or to 3 integers (month,
day, year).
Function Calls.
---------------
You should read the documentation for CSWITCH, the multitasking
functions used by Falken. Functions to control message passing,
semaphores, loading new tasks, and preventing task-switching from
occuring at critical times, are all provided.
The following function calls are available to all doors via doorutil.c
int isinstalled(void)
This function is called by init(), and it checks to see if the
Falken BBS is running. If not, a message is printed, and the
door exits.
int init (void)
init() performs all the doors initialization required, including
attaching to the common memory area for acct and user
structures, loading the global variables with the correct data,
and checking that Falken is up and running.
The return value is the line number (global variable 'who') that
the door is started for.
void logoff (int line)
Logs off the user on the specified line.
void setbinarymode (int line)
This function tells Falken to put the line into binary mode.
Binary mode means that your door is now responsible for reading
and writing to the user's modem. Fossil calls must then be used
to communicate with the user's line.
If you don't know what that means, don't use this call!
void settextmode (int line)
Puts the line back in 'text' mode, where Falken processes user
input and makes it available via the qgets() functions.
This is the normal mode of the line when the door runs, and
under most circumstances, there is no need to change it.
void clear_input_buffer (int line)
Causes Falken to discard all data in the line's input buffer.
void clear_output_buffer (int line)
Causes Falken to discard all data in the line's output buffer.
void get_port (int line)
Use this function to gain control of a specific line from
Falken, for instance, to dial to another BBS. The line will be
given to your program in binary mode.
void return_port (int line)
Return a line, obtained with get_port(), to Falken.
int get_oba (void)
Returns the number of bytes available in the output buffer for
line 'who'. This is 16384-(bytes in buffer).
If you wish to call this function for a line other than the line
that started the door, change the value of 'who' before calling
this function.
int get_in_cnt(void)
get_in_cnt() returns the number of bytes waiting in the user's
input buffer. Falken will buffer the input until the user has
pressed ENTER, at which time the entire buffer will be sent to
the door on the message queue.
int bbslog (char *s)
Writes the string pointed to by 's' to the bbslog.txt file and
to the log window of the BBS monitor.
void lprintf (char *fs,...)
A variable-argument version of bbslog. It performs a
printf()-like processing of the format string, just as printf
does, and the result is sent to bbslog().
int qprintf (char *fs,...)
a printf() function that sends output to the current user, as
determined by the global variable 'who'.
int qputs (char *fs)
Sends a null-terminated text string to the current user, adding
a new line sequence at the end. The current user is determined
by the value of the 'who' variable.
int send (char *fs)
Same as qputs, but does not add a newline sequence.
int sendtoline (int w, char *fs)
Same as send, but output goes to line 'w', instead of line 'who'.
void broadcast (char *fs, int *linenums)
Send the null-terminated text in 'fs' to the lines listed in the
array linenums. linenums is an array containing all the lines
to receive the text, and a -1 after the last line.
For example :
myfunc()
{
char *str="Hello World.";
int dest[10];
dest[0]=1;
dest[1]=3;
dest[2]=7;
dest[3]=-1;
broadcast(s,dest);
}
This will send the text 'Hello World.' to lines 1, 3, and 7.
void sendmsg (int line, int msgnum)
Send the canned message identified by 'msgnum' to the speficied
line. The canned messages are in the files msgtext.msg,
msgansi.msg, and msgrip.msg.
void getmsg (int ansiflag, int msgnum, char *fs)
Get a copy of the message 'msgnum' into the character string
'fs'. If ansiflag is 0, get it from msgtext.msg. If ansiflag is
1, get it from msgansi.msg. If ansiflag is 2, get it from
msgrip.msg.
int qgets (char *s, int len)
Wait for a line of input from the current line, as specified by
'who'. This function will return when the user hits ENTER. The
string 's' will be filled with the null-terminated input,
without carriage return. The return value is the length of the
returned string, which may be 0 if the user just pressed ENTER.
int tqgets (char *s, int len, int timeout)
Same as qgets, but waits 'timeout' seconds, and if no input is
received, returns a 0.
void waitforempty (void)
Wait until all pending output for the current line, as specified
by 'who', has been transmitted.
int getserialnum (char *s)
Gets the serial number of the Falken BBS and places it in s.
int getversion (char *s)
Gets the version number of the Falken BBS and places it in s.
int btrv (int op, int file_id, void *addr, int *len,
ENTRY * key, int keynum)
Calls the Falken database interface function. See the separate
section on Database Functions.
void btupag (int lines)
Sets the number of lines on the user's output screen. If any
single block of text spans more than 'lines' screen lines, the
user will be sent the 'Press [ENTER] to continue message.'. If
'lines' is 0, then paging does not occur.
void setwatchdog (int value)
Tell Falken BBS to monitor this line for carrier drop (value !=
0), or to stop monitoring for carrier drop (value = 0).
void btuech (int echoval)
Set the echo type for the current line (who).
echoval = ECHO_ON - normal echo
echoval = ECHO_OFF - no echo
echoval = ECHO_DOTS - echo dots (used when password is
requested)
int find_account (char *s1, void *mem)
Loads the account database record for the user whose handle or
account number is in s1. mem should be the address of an
acct_rec structure.
int start_a_door (char *cmd)
Calls load_a_door and wait_for_door.
void wait_for_door (int pid)
Wait until the child process 'pid' terminates.
int load_a_door (char *cmd)
Loads the task specified in the string 'cmd'. The pid of the
new child process is returned. -1 is returned on error.
A possible side-effect of using this function is system lockup,
if the caller is careless. The initialization event is sent to
the new task so that the init() process in the new child can
load the proper variables. If the child task is not a Falken
door, then the caller must remove the initailization event
itself by calling recvmsg(), described in the CSWITCH
documentation.
void save_acct (void)
Save the account information for the user on line 'who'. This
is useful if your door alters the account information.
int sendtoq (char *fs, int qnum)
Send the specified character string in fs, to a specific message
queue. This can be used to send messages between one door and
another.
int finduser(char *text, char **ret_text)
This function is used to find the line number a particular user
is on. This function is used in the '.send' commands and other
global commands where a user's name is used. If the user is
online, the return value is the line number they are on.
text is a pointer to a character string which contains a user
name followed (optionally) by some text. ret_text should be the
address of a character pointer, which will be loaded with the
address of the first byte following the user name. Example :
char *p;
finduser("herb hello there",&p);
This will return the line number that 'herb' is logged onto, and
will load 'p' with the address of the ' ' following 'herb'.
Database Functions
------------------
Falken uses a very efficient B+tree file manager and data manager
interface which is built into the mainbbs.exe program. External
modules can take advantage of Falken's database capabilities via a
database API which is included in doorutil.c
A brief review of Falken's database capabilities is in order.
Falken's database is a single-key, B+Tree index associated with
variable-length records. Each database consists of an index file,
which is managed by the B+Tree routines for fast access, and a data
file which is simply a flat file containing data records. The index
file contains KEYS, which are null-terminated character strings, and
VALUES, which indicate the position of the data record in the flat
file.
Neither the index file nor the flat file is directly editable. The
index file is a binary file, and contains a multi-level, balanced
B+Tree. The data file may contain ordinary text, but each record is
preceeded by a 2-byte value containing the number of bytes in the
record. This 2-byte value is a binary number, and cannot be edited.
Functions which can be performed on the databases are :
+ open a database
+ close a database
+ create a database
+ insert records into a database (key and data)
+ insert only data records into a database
+ search a database, looking for exact key matches
+ search a database, looking for a key greater than or equal to
a key value
+ search a database, looking for a key less than or equal to a
key value
+ find the first record in a database
+ find the last record in a database
+ retrieve the next or previous record in a database
+ update a database entry
+ delete a database entry
All functions are performed via a call to the btrv() function in
doorutil.c . The specific function that you wish to perform is
included as a parameter.
If there are no errors, btrv returns OK. A negative value on the
return indicates an error. If the b_open function is being used, a
non-negative return is the new database handle.
The btrv function has the following prototype :
int btrv(int function, int handle, void *data, int *size, ENTRY *key,
int keynumber);
data : the address of the first byte of data to write, or the
address to place the data during read operations.
Normally, this will be the address of a structure of the
specified type, such as the address of an acct_rec
structure or email_rec structure.
size : this is the address of an integer. For write operations,
put the number of bytes to write, often written as
sizeof(struct ...). For read operations, the variable is
loaded by the btrv function with the number of bytes that
were read from disk. This is why the address is passed,
so that btrv() can fill it with the bytes read.
key : this is the address of an ENTRY variable. ENTRY is a
typedef for a structure of the type :
typedef struct
{
unsigned long recptr;
int reclen;
char key[32];
} ENTRY;
recptr is the VALUE portion of the B+Tree database, and is
normally the offset within the data file where the record
for this key begins. reclen is the number of bytes of this
record. key is a char array containing a null-terminated
string which is the key for this record.
keynumber : this field is only used by the b_create and b_open
functions at present. In these calls, a 0 in this field
indicates that duplicate keys are not allowed. A non-0
indicates that duplicate keys are allowed.
Future modifications may include the ability to have
multiple keys in a database. In that case, this variable
will be used to identify the key to search. Always set it
to 0 for read and write operations for now.
handle : the database identifier. Falken has at least 6 databases
open at all times. User modules may open more. The
database handle identifies the database you wish to access
with this call to btrv(). The database handle is returned
by the btrv() function when b_open or b_create is the
function.
There are 6 special database handles - the handles for the
6 databases that Falken has permanently opened. These are :
ACCT_FILE - user accounts, indexed by handle
DLOAD_FILE - download files, indexed by lib & name
EMAIL_FILE - email file, indexed by handle
MSG_FILE - message base file, indexed by folder
BIOS_FILE - user bio file, indexed by handle
ACCT_NUM_FILE - account numbers file, indexed by acct #
These databases should not be opened or closed by the door
module - simply assume that they are open and available,
and include the above constant as the handle for the btrv
call.
function : one of the following constants :
b_open - open an existing database. Use (-1) as the handle, and
provide the name of the index file as the data pointer and
the name of the data file as the key pointer. The
'keynumber' parameter should be 0 to indicate that
duplicate keys are not allowed, and 1 to indicate that
duplicate keys are allowed. Example :
myhandle=btrv(b_open,-1,"mydb.idx",&size,"mydb.dat",1);
size=1000;
btrv(b_getlow,myhandle,dataptr,&size,&mykey,0);
This example opens a database called 'mydb', with duplicate
keys allowed. It then uses the handle returned from the
b_open to read the first record in the database.
b_create - create a new database. This call is exactly like
b_open, except that it will create new idx and data files,
and destroy those files if the already exist.
b_close - the only parameter to this call that is used is the
handle. It closes the specified database. Example :
btrv(b_close, myhandle, NULL, NULL, NULL, 0);
b_insert - insert a new record into the database. The key
parameter contains a pointer to the ENTRY structure which
has the key value to store in the index file. The data
pointer is a pointer to the data. The size parameter is
the address of an integer containing the number of bytes to
write to the data file. Example :
int size;
ENTRY mykey;
char somedata[100];
strcpy(mykey.key,"key1");
size=100;
btrv(b_insert,myhandle,somedata,&size,&mykey,0);
This writes 100 bytes of data into the database identified
by 'myhandle'. This data can be retrieved by performing
the 'b_getequ' function with the value "key1" in mykey.key.
b_getequ - find an exact database entry, based on the key.
int size;
ENTRY mykey;
char somedata[100];
strcpy(mykey.key,"key1");
size=100;
btrv(b_getequ,myhandle,somedata,&size,&mykey,0);
In this case, the size variable is loaded with the MAXIMUM
number of bytes to read, and the mykey.key variable is
loaded with the key to find. If there are multiple
instances of this key, it will return the first one in the
file. The rest can be retrieved with the b_getnext
function.
If no entries exist for the specified key, an error is
returned. Otherwise, the return is OK.
b_getgt - find the first key whose value is > the specified key.
b_getlt - find the first key whose value is < the specified key.
b_getge - find the first key whose value is >= the specified key.
b_getle - find the first key whose value is <= the specified key.
b_getlow - find the first key (lowest value) in the database
b_getgt - find the last key (highest value) in the database
b_getnext - find the next key in the database.
b_getprev - find the previous key in the database.
For the b_getnext and b_getprev functions to work
correctly, there must be a preceeding b_get* call
(b_getequ, b_getlow, etc) to position the database. The
values in the ENTRY variable are used by Falken to traverse
the index file, so they must not be altered between the
calls.
Some examples ...
To read ALL entries in a file..
int status;
int size;
ENTRY mykey;
char somedata[100];
size=100;
status=btrv(b_getlow,myhandle,somedata,&size,&mykey,0);
while(status==OK)
{
.. do some work ..
size=100;
status=btrv(b_getnext,myhandle,somedata,&size,&mykey,0);
}
To read ALL entries in a file for the key 'Herb' :
int status;
int size;
ENTRY mykey;
char somedata[100];
size=100;
strcpy(mykey.key,"Herb");
status=btrv(b_getequ,myhandle,somedata,&size,&mykey,0);
while((status==OK) && (strcmpi(mykey.key,"Herb")==0))
{
.. do some work ..
size=100;
status=btrv(b_getnext,myhandle,somedata,&size,&mykey,0);
}
Note that the b_getnext and b_getprev calls use the ENTRY
variable without loading any new values. All functions
that retrieve data from the database, such as b_getequ
b_getnext b_getlow etc, store the current key value, key
location, and data pointer, into variables within the
ENTRY structure. These values are then used to
accurately position the database for the next call.
b_getdirect - this is a variation of the b_getequ function. It
finds the database record that matches both the key and the
recptr value that you load into the ENTRY structure. If
there are multiple records with the same key, (several emails
for the same user, for example) then b_getequ will always
return the first record that matches the key, in effect
always returning the first record in the set of records that
has the same key. If you wish to access a SPECIFIC record
within that set, without using the b_getnext loop, and if you
have previously saved the recptr value from a btrv() call for
that record, you can read it directly. Such as :
size=100;
strcpy(mykey.key,"Herb");
mykey.recptr = oldrecptrvalue;
status=btrv(b_getdirect,myhandle,somedata,&size,&mykey,0);
b_update - modify an existing record. Example :
int status;
int size;
ENTRY mykey;
char somedata[100];
size=100;
strcpy(mykey.key,"Herb");
status=btrv(b_getequ,myhandle,somedata,&size,&mykey,0);
if(status==OK)
{
somedata[0]='Q'; /* make a change.... */
/*
** leave the mykey structure alone, and the size
** value alone, since they were set by b_getequ
*/
status=btrv(b_update,myhandle,somedata,&size,&mykey,0);
if(status < 0)
{
qprintf("Error %d in b_update.\r", status);
}
}
else
{
qprintf("Error %d in b_getequ.\r", status);
}
b_append - this function is used to write only the data portion
of the database. No key is written to the index file. This
may sound like it is of little use, but it has it's uses. For
example : the message base database contains records which
are simply arrays of pointers to the message base text. The
actual message base text is stored using the b_append
function, and the keyed records contain the pointers to these
text records. This takes a little extra effort on the part
of the programmer, but can be very efficient.
The offset at which the data was written is loaded into the
recptr member of the ENTRY structure by btrv().
b_getdata - this function is used to read data directly from the
data file. The recptr member of the ENTRY structure is
loaded with the position in the file to start reading, then
the btrv() function is called. The data file is read
directly at the position specified.
This is logically the opposite of b_append, and normally, the
value returned in the recptr member of ENTRY by the b_append
call will be used as the recptr position in this b_getdata
call.
Message Base File Format.
The Message Base file (MSGBASE.IDX, MSGBASE.DAT) contains both
control records and data records. The control records are in the
format 'msg_record' in structs.h. Basically, it works like this :
Each message has a 'msg_record' structure in the database.
The msg_record structure contains the message number, time stamp
of the last update, subject, and NUMBER OF RESPONSES.
The msg_record also contains a 'resp' structure for each response
to the message (the original message is resp[0]). The
'resp' structure contains the user name that wrote the
response, time stamp of the response, and an unsigned long
called msgsequence.
MSGSEQUENCE is the offset within the data file to read the text.
As with all database entries, there is an integer containing the
size of the text, followed by the text itself.
To read the message base records - read the control record
(msg_record) first, then for each response (respcnt), read the
text pointed to by the msgsequence member of the resp structure
for that response.
Use the 'b_getdata' function when calling BTRV() to read from a
specific location in the data file.
Nothing to it.
A note : when searching for NEW posts, Falken first examines the
'last_update' field in msg_record, and if that time stamp is newer
than the users time stamp in that folder, the message is selected
for reading. Then, each response is checked for a time stamp
later than the users time stamp. Text is only read when the time
stamp for that response is later than the users time stamp.
Information about the USERBIO and Email List structures
/*
** Questionaire processor for Falken.
** The sysop can change the questions around, by changing the questions in
** messages.msg numbered MC_USERINFO_Q1 - MC_USERINFO_Q5
** If less than 5 questions are asked, then the end of the questions list
** is marked with the message '$END'
**
** The display of the answers is governed by the message MC_USERINFO_FORMAT
** This is a normal text display, with fields filled in with the answers
** provided by users. Text to be filled with answers is in this format
** %2 %
** where everything between the % signs is filled in with answer #2.
**
** The user is then given up to 15 lines of freeform text to say something
** about themselves.
*/
struct bio_record
{
char username[uidlen];
char ans[5][60];
char text[15 * 80];
};
The mail list record is stored in the user bio database,
using the owner's name as the key, with a '@' in front...
such as '@herb'
struct mail_list_rec
{
char owner_id[uidlen];
char listname[20];
int entrycount;
char list_entry[64][32];
};
Falken uses the C/Database Toolchest from MIX software to provide
the B+Tree functionality. I have written the btrv() function which
acts as the Falken API to the database functions. If you wish to
write offline utilities, such as utilities to clean up or compact
database files, I will provide the btrv() object file, to be linked
with the C/Database Toolchest library, for a stand-alone DOS
executable for database access.
MIX provides excellent quality programming tools at incredibly low
prices. I highly recommend them.
Introduction to Falken Serial I/O capabilities.
-----------------------------------------------
Falken's serial I/O capabilities are provided by an internal
interrupt handler. It is not necessary (or smart) to load an
external program to control the serial ports. Falken uses an
'interrupt-driven poll' method to control the serial ports. In
short, this is what happens :
Falken examines the hardware configuration, and determines the
highest baud rate used by the system. It then calculates how many
times per second it must test the serial hardware to see if any of
the modems need servicing. The timer chip in the PC is then
reprogrammed to interrupt Falken at that interval, and Falken
queries the appropriate serial ports, looking for ports that require
servicing (character received, transmit ready, DCD change, etc.).
This method overcomes a very serious shortcoming in PC hardware
design - there are only 2 IRQ vectors assigned to serial ports. If
more than 2 serial devices are required, they must share these IRQ
vectors. If the devices that are sharing a vector are on different
cards (2 or more internal modems, for instance), then interrupts can
be lost. So Falken does not use the normal serial I/O interrupts.
In fact, the IRQ setting of the serial device is irrelevant, and not
used by Falken at all.
Falken's serial driver is Fossil compatible. This means that the
driver conforms to most of the Fossil specification found in the
FOSSIL.DOC file included with this archive. It supports a 'text'
mode also.
In 'text' mode, Falken will :
Echo incoming characters (keystrokes) back out to the port.
Filter input according to an internal translation table.
Characters to be echoed back to the port are held until the
current output has completed. If you start typing in the middle
of a menu, for example, your keystrokes will not be echoed until
the current transmission is completed.
On output, Line Feed characters are ignored, but a line feed is
automatically transmitted after every carriage return, if the
user configuration specifies to do so.
Backspace is echoed as '<BS><SPACE><BS>', so that a BS is
destructive, but a BS will only be echoed if there are
characters in the input buffer.
Characters in the output buffer are not transmitted while there
are characters in the input buffer. This prevents the user's
input from being intermixed with output text.
The net effect is a more fluid user interface. Once the user's
input starts echoing out, it will not be interrupted by output from
Falken. Aborted output will not terminate in the middle of a line of
text. Output from Falken will not be interrupted by echoed
keystrokes, until the current 'record' is completed.
A door program that receives input from Falken via message
queues does not have to worry about echoing characters or cr/lf
conversions, etc. The line will still be in 'text' mode, since
Falken is still in control of the port. This is not true if the
door uses the port directly.
If a door program requests direct control of the serial port,
the port is placed into 'raw' mode. All 'text' mode controls
are disabled, and the program is given a raw serial port which
responds to Fossil calls.
To take direct control of the serial port, a door *MUST* inform
Falken that it wishes to do so. Otherwise, both Falken and the
door program will be trying to obtain input and send output to
the same port, and very likely nothing will work right. To gain
control of the serial port, call the setbinarymode() function in
doorutil.
As soon as Falken receives the binary mode request, it places
the port into binary mode (i.e. takes it out of text mode), and
relinquishes all control of the port. When the door stops,
Falken takes the port back. The only real checking that Falken
does while the door has control of the port, is to check for
Carrier Detect. If DCD drops, Falken will clean up the port and
terminate the door.
See the file transfer protocols for examples of taking control
of the serial port.
Falken's built-in FOSSIL support provides the following standard
FOSSIL services to doors that have control of the serial port :
Function 0 : Set Baud Rate (will not work if port is FIXED BAUD)
Function 1 : Transmit Character with wait
Function 2 : Receive Character with wait
Function 3 : Status Request
Function 4 : Initialize Port
Function 5 : De-initialize Port
Function 6 : Raise/Lower DTR
Function 7 : Return System Timer Parameters
Function 8 : Flush Output Buffer
Function 9 : Purge Output Buffer
Function A : Purge Input Buffer
Function B : Transmit Character, no wait
Function C : Non-destructive 'peek' at input buffer
Function F : Flow Control
Function 18 : Read Block
Function 19 : Write Block
Function 1A : Start/Stop Break Signal
Function 1B : Get Driver Information
Functions 1, 2, and 8 specify that the Fossil driver will not
return to the caller until the condition is satisfied. In this
case, the Fossil driver will use the Cswitch RELINQ() call to
allow other tasks to run until the condition is met.
Performance is not affected by using these calls.
;
; Description of functions in FSUBS.ASM
;
; This is aset of C-linkable routines, in large model, for directly
; calling a Fossil driver.
;
function name : f_recv
Callinq sequence : f_recv(int line, char *buffer, int maxcount)
Description :
Reads up to 'maxcount' bytes from the specified line. The bytes
are placed into 'buffer'. If fewer than 'maxcount' bytes are
available in the input buffer, they are all retrieved.
Returns the number of characters placed into buffer.
function name : f_send
Callinq sequence : f_send(int line, char *buffer, int count)
Description :
Sens 'count' bytes to the specified line for transmission. Returns
immediately, without waiting for characters to be transmitted.
Returns the number of characters placed into the Fossil output buffer
for transmission. If there is not enough room for all the characters
to fit, the returned value will be less than 'count'.
function name : f_status
Callinq sequence : f_status(int line)
Description :
Returns the current status of the specified line.
See FOSSIL.DOC for a description of the status byte.
function name : f_setbaud
Callinq sequence : f_setbaud(int line, int param)
Description :
Uses FOSSIL function 0 to set line parameters to 'param'.
See FOSSIL.DOC for more details.
Usually, a door task will not need to call this function.
function name : f_initialize
Callinq sequence : f_initialize(int line)
Description :
Uses FOSSIL service 4 to initialize the port.
Usually, a door task will not need to call this function.
function name : f_dtr
Callinq sequence : f_dtr(int line, int onoff)
Description :
Sets the DTR (Data Terminal Ready) on or off for the specified port,
according to whether 'onoff' is zero or non-zero. A zero value
turns DTR off.
A door program usually will not need to call this function.
function name : f_flushin
Callinq sequence : f_flushin(int line)
Description :
Uses FOSSIL service 0A to flush the ports input buffer.
All characters in the input buffer are discarded immediately.
function name : f_flushout
Callinq sequence : f_flushout(int line)
Description :
Uses FOSSIL service 9 to flush the ports output buffer.
All characters in the output buffer are discarded immediately, and
are not transmitted.
function name : f_flowctrl
Callinq sequence : f_flowctrl(int line, int flow)
Description :
Uses FOSSIL service 0F to set flow control parameters for the line.
See FOSSIL.DOC for a description of 'flow'.
function name : f_purgeout
Callinq sequence : f_purgeout(int line)
Description :
Uses FOSSIL service 8 to wait until the output buffer is empty.
function name : f_writechar
Callinq sequence : f_writechar(int line, int data)
Description :
Uses FOSSIL service 1 to write 'data' out to the line. If there is
not room in the output buffer for one character, this service waits
until room is available.
function name : f_readchar
Callinq sequence : f_readchar(int line)
Description :
Uses FOSSIL service 2 to read a character from the input line. If
no characters are available, it waits until a character is received.
Returns the next character from the line's input buffer.
function name : f_getibw
Callinq sequence : f_getibw(int line)
Description :
Returns the number of characters waiting in the line's input buffer.
function name : f_getoba
Callinq sequence : f_getoba(int line)
Description :
Returns the number of characters that can be written to the file's
output buffer.
function name : f_insertchar
Callinq sequence : f_insertchar(int line, int chr)
Description :
Inserts the single character 'chr' into the input buffer for the
specified line.
BINARY MODE Considerations.
---------------------------
The Falken low-level communications routines do a lot of work
while in text mode. Word-wrapping of output, translation of CR
to CR/LF if the user requires it, masking of control codes on
input, that kind of thing. In Binary mode, Falken does not do
anything to the data going in or out! This means that there
will be no word-wrap, no echo, no CR to CRLF translation, no
filtering of input, in other words, no text handling of any
kind. Your task will have to make FOSSIL calls to determine the
status of the line, read input data, and send output to the
line. Falken will monitor Carrier Detect (DCD) from the modem,
and will hang up the line and terminate the door if DCD drops.
If you are writing a program that will work off 'hot keys', that
is, one that requires only a particular key to be pressed,
without hitting ENTER, then you will need to use binary mode.
Also, if you wish to write a new file transfer protocol for
Falken, you will have to use Binary mode.
For most programs, however, leaving the line in text mode is the
easiest, best way to go. Since Falken will handle the
nitty-gritty
PORTING DOORS TO FALKEN.
------------------------
It has been my experience that doors written for other BBS
programs do not work well with Falken. Most doors written for
other BBSes assume that they have complete control of the
hardware, and insert their own interrupt handlers and such.
Other programs assume they are running under one specific BBS
program, and perform functions unique to it, such as Wildcat's
logging functions.
To port a door to Falken, make sure it uses Fossil-style calls
to the serial port (INT 14H). Also, make sure that code is
written to read the initialization event from Falken and use the
information it contains.
Some doors require a text file which contains the information
needed by the door. CALLCNT.BBS and WWIVs CHAIN files are
examples. It is possible to use these doors directly, with a
little work. You need to write a 'loader' program which will :
1. Read Falkens initialization message, and pull the information
required by the door from the various structures this gives
you access to. This is done with the init() function.
2. Get direct control of the serial port (setbinarymode())
3. Write the text file in the correct format for the door.
4. Use the EXECTASK() call to replace your 'loader' task with
the real door. The door will inherit your tasks files and
Falken will not even realize that another door has taken the
place of the one it started. At this point, the real door
can make Fossil calls to control the port, and everything
else it needs to do.
RUNNING DOS TASKS.
------------------
Falken also supports I/O redirection for STDIN/STDOUT. If you
have a program that runs under DOS, and does not write directly
to screen memory, and does not read keys directly from the
keyboard, then Falken can run it as a door, and redirect input
and output to the serial port. To load such a program, put the
word 'dos' in front of the program name in the menu.
Like this : 'dos myprog.exe'
In general, if you program uses the printf(), puts(), putchar(),
scanf(), gets(), getchar() type functions for I/O, this will
work. In fact, I have successfully run COMMAND.COM as a door
under Falken. This is not recommended, but it is possible.
WRITING MULTI-PLAYER GAMES.
---------------------------
There are 2 possible approaches to writing multi-player games.
The first approach is to run a copy of the game for each player,
and communicate between the various tasks using message queues.
The other method is to run a single copy of the game, and have
it perform all communication for all players.
The first method will work pretty well. The user_rec structure
for each line contains the queue numbers to be used for I/O with
that user. It is a simple matter to format an information
message concerning your players moves, and send it to the
program being run for the other players. The Connect-4 game
works this way.
-------------- _________
| | ==== | Door 1 | Only door 1 communicates with
| Falken | |_________| user 1
| | | ^
| BBS | | | Information passes between doors
| | _v____|___
| Program | ==== | Door 2 | Door 2 communicates with user 2
| | |__________|
--------------
The other method requires some explanation.
Falken monitors the status of the door program that is loaded
for each line. If the door program terminates for any reason,
Falken returns the user to the menu or area he was in prior to
activation of the door. Also, if the user drops carrier or exits
back to the BBS, Falken will terminate the door as quickly as it
can, if the door continues to try to run.
Obviously, it is not feasible for multiple users to be serviced
by a single program. There are times, however, when a single
program handling multiple users is the ONLY way to go! A
solution ....
Write a small door to be loaded and run when the user wants to
enter the game. This small door will examine the user_rec
array, and determine if any other users are already running the
door. If not, it will load the program using the 'LOADTASK()'
call. The initialization event will be sent to the program
using queue #1, just as if it were sent by Falken.
The small door will now perform a 'SUSPEND()' call. This leaves
the task active, but removes it from execution. It will stay
dormant until another program issues a 'WAKEUP()' call for it.
This satisfies Falkens check of program status for the user.
The *REAL* game will loop, examining the the user_rec array,
looking for players that are currently in the game. This is
done by examining the 'u_stat' variable (6 means the user is
running an external program), and the 'doors_id' array (a char
array with the name of the door being run). For each user that
is currently in the game, check for data on that user's input
queue. Send outgoing text to that players output queue.
-------------- __________
| | | Door 1 | When real door is loaded, SUSPEND()
| Falken | DORMANT |
| | ---------- ______________________________
| BBS |====================| Real door communicates with |
| |====================| all players currently in game|
| | __________ ------------------------------
| Program | | Door 2 | When real door is loaded, SUSPEND()
| | | DORMANT |
-------------- ----------
When a player quits the game, or when the player's u_stat
becomes != 6, issue a wakeup() for that users dormant task. The
TCB number is in the user_rec structure (u_doortcb). i.e.
wakeup((*user)[who].u_doortcb); The dormant task should exit
immediately after the SUSPEND() call. i.e.
HOG();
copy a unique string into (*user)[who].doors_id to show we are
playing the game. real program will look for doors_id with
this 'signature';
If (real program not loaded)
{
LOADTASK();
send a copy of initialization event on queue #1;
}
NOHOG();
SUSPEND();
exit(); /* or endtask(), if you have an endtask() function */
/* note - upper case names are for clarity only */
/* use lower case in your program */
The HOG() function is used to temporarily prevent other tasks
from running. You should do this when checking if the real
program is loaded, to prevent 2 doors from trying to load the
real program simultaneously. The NOHOG() will allow
multitasking to proceed normally.
I have used this method to write multi-user programs. I try to
keep my programs from wasting processor time, so I program my
'idle loop', to scan each users input queue one time, then I
suspend execution for a while with either a sleep() or relinq()
call. I do not have an infinite loop running without
relinquishing control of the processor in some way.
When the last user has left the game, the *REAL* program simply
exits.
Examine the programs LOADTLCF.C and TLCFDEMO.C for a real
example of using this method for implementing a multi-player
program.
Tricks that DOORS Play with Memory.
-----------------------------------
This section is important if you wish to write SMALL MODEL
programs for Falken. If you use LARGE model programs,
everything will work just fine with no extra effort on your
part.
First, it is important to understand the difference between
memory models on the 80x86 processors. Since registers on the
8086/8088 processors are only 16 bits wide, including the
Instruction Pointer register, there is an address limitation of
64 Kbytes (2^16). To overcome this limitation, Intel uses 4
special registers called SEGMENT registers to augment
addressing. Each segment register defines a BASE ADDRESS, with
a resolution of 16 bytes. Each register, including the
INSTRUCTION POINTER, provides an offset from that base address,
from which the instruction or data is retrieved. In actual
practice, the segment is loaded as bits 4-19 of the 20-bit
address, with bits 0-3 set to 0, then the offset is added to it,
like this :
Segment register x x x x x x x x x x x x x x x x 0 0 0 0
Offset x x x x x x x x x x x x x x x x +
---------------------------------------
Actual address x x x x x x x x x x x x x x x x x x x x
There are 4 segment registers. They are :
CS - Code segment register. Normally combined with the IP to
fetch the next executable instruction.
DS - Data segment.
Normally used to access memory. Most instructions that
manipulate memory in any way use the DS register by
default.
SS - Stack segment. All operations concerning the Stack
Pointer and Base Pointer use SS by default.
ES - Extra Segment. This register is useful as an extra
segment register. It is often used in conjunction with
the DS register when moving data between segments.
Since the segment register identifies base addresses on a
16-byte (paragraph) boundary, and there are 64K possible values
for the segment registers, we may address up to 1024K (64K * 16)
or 1 Megabyte with the 8086/8088 processor, as well as a
80286/80386/80486 operating in REAL mode.
Why do we need to know this low-level esoterica? Because
compilers and assemblers for the 80x86 family of processors use
these segment registers differently, based on the Memory Model
chosen for the program.
A SMALL memory model program will allow 64K data and stack, plus
64K code. It sets the CS, DS, and SS registers to point to the
beginning of each of these memory regions, and does not change
them while the program is running. Memory accesses are
performed using 16-bit arithmetic for the addressing. Only the
IP is pushed onto the stack for a subroutine call, and all
pointers require only a 16-bit offset to identify the datum to
which they refer.
A LARGE memory model program may have both code and data
segments larger than 64K. In this case, memory accesses must
specifically load the segment registers with the paragraph
address of the datum to be accessed, since the datum may be in a
different 64K segment. Pointers must contain both the 16-bit
offset, and the 16-bit base address (segment) to identify their
datum. Subroutine calls must push both the IP and the CS onto
the stack, and the CS and IP are both replaced by the 32-bit
address of the target.
Medium and Compact memory models are variants of these basic
types, allowing either code or data segments to be larger than
64K. We will not concern ourselves with these models.
The above discussion is important for Falken Door authors,
because Falken passes some information to doors as POINTERS. To
be specific, Falken passes Large Model pointers (called FAR
pointers) which contain both the 16-bit offset of data and the
16-bit segment address of the data. If you write programs in
the SMALL memory model, there may be problems because of the
different pointer sizes.
An example is the copying of the users name to a local string.
Falken passes the address of an array of structures, each of
these structures contains one account record for the configured
ports. Falken also tells you which port YOUR user is on. I use
the variable 'who' to identify the port I am running for, and
the pointer 'acct' to point to the address of account record
structures. To copy the users name into a local string called
'name', you might code a line like this :
strcpy(name, acct[who].acctname);
The example instruction above should work normally. If you have
written your program in SMALL model, though, it will not. The
problem is that the strcpy function in the small model library
cannot accept a far address for one of it's arguments. The
instruction will appear to work, but in fact will only use the
offset of the account records structure, and index from YOUR DS
register. The result will be corruption of your program, or
possibly, of someone elses program. System lockups will surely
result.
If you built your program with LARGE model, there would be no
problem, since the strcpy function in the large model library
will accept 32-bit addresses for both arguments.
So, how do you copy the account name if you have a small model
program? The best way is to write your own copy routine. Like
this :
far_strcpy(char far *dest, char far *src)
{
while(*dest++ = *src++);
}
When you call the far_strcpy() function, cast both arguments as
(far char *) to make sure both pointers get passed as 32-bit
pointers, like this :
far_strcpy((char far *)name, (char far *)(*acct)[who].acctname);
DO NOT IMPLEMENT FAR_STRCPY AS A MACRO!!!!!
It is quite possible, and easy, to write small model programs as
Falken Doors. You must use great care to assure that any data
gotten via the pointers Falken passes to your program is
retrieved as FAR data.